module('lgx.declset', package.seeall)
local decl     = require'lgx.decl'
local codecell = require'lgx.codecell'
local strbuf   = require'lgx.strbuf'
local utils    = require'lgx.utils'
mt = {}
mt.__index = mt
mt.__newindex = function (tab, key, value)
    if rawget(tab, key) then
        rawset(tab, key, value)
    else
        for i,v in ipairs(tab) do
            v[key] = value
        end
    end
end

local idmap = {}
idmap.__index = idmap

function idmap.new(o)
    o = o or {}
    setmetatable(o, idmap)
    return o
end


------------------------------------------------------------------------
--  give a string like '_1 _2 _3...' as idlist, return an array which
--  include decl whose 'id' is '_1' '_2' ...
------------------------------------------------------------------------
function idmap:list(idlist)
    local array = {}
    for id in string.gmatch(idlist or '', '_[^%s]+') do
        table.insert(array, self[id])
    end
    return array
end

function idmap:nsname(id, prefix, suffix)
    if not self[id] then return '' end
    return self[id]:nsname(prefix, suffix)
end

local function functypestr(ftdecl)
    local tmpl = '$(return) (*)($(args))'
    local info = ftdecl:typeinfo(ftdecl.returns)
    local ret  = info.name
    local args = strbuf.new()
    for i,v in ipairs(ftdecl.args) do
        info = ftdecl:typeinfo(v.type)
        args = args + info.name
    end
    return utils.template(tmpl, {['$(return)'] = ret, ['$(args)'] = args:concat(', ')})
end

------------------------------------------------------------------------
--  typeinfo() will return a type's detail info in a table:
--  {
--      name     = declaration in c++ code,
--      rtype    = if type is refrence type then true else false,
--      cvtype   = ...........const..............................
--      ptype    = ...........pointer............................
--      tdef     = if type is a typedef then typedef's name else nil,
--      mtype    = if type is memfunction pointer then true else false,
--      atype    = if type is array type then true else false,
--      functype = if type is function pointer then true else false
--  }
------------------------------------------------------------------------
function idmap:typeinfo(typeid, info)
    local info = info or {name=''}
    local typedecl = self[typeid]
    assert(typedecl, string.format('type "%s" not found', typeid))
    if typedecl.tag == decl.RTYPE then
        info.rtype = true
        info.name = info.tdef and info.name or '&'..info.name
        return self:typeinfo(typedecl.type, info)
    elseif typedecl.tag == decl.CVTYPE then
        info.cvtype = true
        info.name = info.tdef and info.name or ' const'..info.name
        return self:typeinfo(typedecl.type, info)
    elseif typedecl.tag == decl.PTYPE then
        info.ptype = true
        info.name = info.tdef and info.name or '*'..info.name
        return self:typeinfo(typedecl.type, info)
    elseif typedecl.tag == decl.TDEF then
        info.tdef = typedecl:nsname()
        return self:typeinfo(typedecl.type, info)
    elseif typedecl.tag == decl.MTYPE then
        info.mtype = true
        return self:typeinfo(typedecl.returns, info)
    elseif typedecl.tag == decl.ATYPE then
        info.atype = true
        return self:typeinfo(typedecl.type, info)
    elseif typedecl.tag == decl.FUNCTYPE then
        info.functype = functypestr(typedecl)
        info.name = string.gsub(info.name, '*', '')
        return self:typeinfo(typedecl.returns, info)
    else --final type
        if not typedecl.name then --class/union/struct with no name
            info.name = info.tdef or ''
            info.decl = typedecl
            return info
        end
        info.name = (info.tdef or typedecl:nsname())..info.name
        info.decl = typedecl
        return info
    end
end

function idmap:typename(typeid)
    local info = self:typeinfo(typeid)
    return info.name
end

function idmap:declfile(id)
    local fdecl = self[id]
    if fdecl and fdecl.tag == decl.FILE then
        return fdecl.name
    else
        return nil
    end
end

function idmap:__tostring()
    return '<<idmap>>'
    --return string.format('<<%d decls>>', table.size(self))
end


function new(o)
    o                  = o or {}
    o.ids              = o.ids or idmap.new()
    o.Class            = o.Class or {}
--    o.Base             = o.Base or {}
    o.Struct           = o.Struct or {}
    o.Function         = o.Function or {}
    o.Variable         = o.Variable or {}
--    o.Argument         = o.Argument or {}
--    o.Ellipsis         = o.Ellipsis or {}
    o.Method           = o.Method or {}
    o.Field            = o.Field or {}
    o.Typedef          = o.Typedef or {}
--    o.EnumValue        = o.EnumValue or {}
    o.Enumeration      = o.Enumeration or {}
    o.Union            = o.Union or {}
    o.Constructor      = o.Constructor or {}
    o.Destructor       = o.Destructor or {}
    o.Namespace        = o.Namespace or {}
    o.OperatorMethod   = o.OperatorMethod or {}
    o.OperatorFunction = o.OperatorFunction or {}
    o.ArrayType        = o.ArrayType or {}
    o.Converter        = o.Converter or {}
    o.OffsetType       = o.OffsetType or {}
    o.FunctionType     = o.FunctionType
    o.MethodType       = o.MethodType or {}
    o.File             = o.File or {}
    o.CvQualifiedType  = o.CvQualifiedType or {}
    o.ReferenceType    = o.ReferenceType or {}
    o.FundamentalType  = o.FundamentalType or {}
    o.PointerType      = o.PointerType or {}

    setmetatable(o, mt)
    return o
end


------------------------------------------------------------------------
--mt:new  create a new declset whose ids = self.ids  
------------------------------------------------------------------------
function mt:new(o)
    o = new(o)
    o.ids = self.ids
    return o
end


------------------------------------------------------------------------
--  
------------------------------------------------------------------------
function mt:include()
    for i,declitem in ipairs(self) do
        declitem.ignore = false
    end
end


------------------------------------------------------------------------
--  
------------------------------------------------------------------------
function mt:exclude()
    for i,declitem in ipairs(self) do
        print('ignored - '..declitem.name)
        declitem.ignore = true
    end
end


------------------------------------------------------------------------
-- check if this set contained declitem, if not, return nil, else 
-- return nonil
------------------------------------------------------------------------
function mt:contained(declitem)
    return self[declitem.tag] and self[declitem.tag][declitem.id] or nil
end

------------------------------------------------------------------------
--  
------------------------------------------------------------------------
function mt:insert(declitem)
    if self:contained(declitem) then return end
    --don't insert declitem into self.ids because self.ids only managed by parser
    table.insert(self, declitem)
    declitem.owner = self
    local tab = self[declitem.tag]
    if tab then 
        table.insert(tab, declitem)
        tab[declitem.id] = declitem --for quick search
    end
end

------------------------------------------------------------------------
--  
------------------------------------------------------------------------
function mt:insertx(declitem)
    if self:contained(declitem) or #declitem.name == 0 or declitem.ignore then return end

    self:insert(declitem)
    local mlist = declitem:idlist('members')
    for i,v in ipairs(mlist) do
        self:insertx(v)
    end
	local baselist = declitem:idlist('bases')
	for i,v in ipairs(baselist) do
		if not v.ignore then self:insert(v) end
	end
    --print(declitem.decl:__tostring())
    assert(not (declitem.returns and declitem.type), declitem.name..' have "returns" and "type"!')
    if declitem.returns or declitem.type then
        local id = declitem.returns or declitem.type
        local info = declitem:typeinfo(id)
        if info.decl then
            self:insertx(info.decl)
        end
    end
    if declitem.args then
        for i,v in ipairs(declitem.args) do
            local info = declitem:typeinfo(v.type)
            if info.decl then
                self:insertx(info.decl)
            end
        end
    end
end

------------------------------------------------------------------------
--  
------------------------------------------------------------------------
function mt:remove()
    --don't remove decl from self.ids because self.ids is for all decls
    local declitem  = table.remove(self)
    if self[declitem.tag] then
        local decl_ = table.remove(self[declitem.tag])
        assert(declitem == decl_)
    end
    return declitem
end

------------------------------------------------------------------------
--  
------------------------------------------------------------------------
local function querybyname(set, array, name, handler, handlerobj)
    local result = set:new()
    for i, declitem in ipairs(array) do
        if declitem.name and (declitem.name == name or declitem.unmangled == name) then
            if handler and handlerobj then
                handler(handlerobj, declitem)
            elseif handler then
                handler(declitem)
            end
            result:insert(declitem)
        end
    end
    return result
end


------------------------------------------------------------------------
--  
------------------------------------------------------------------------
local function querybyfunc(set, array, func, handler, handlerobj)
    local result = set:new()
    for i,declitem in ipairs(array) do
        if declitem.name and func(declitem) then
            if handler and handlerobj then
                handler(handlerobj, declitem)
            elseif handler then
                handler(declitem)
            end
            result:insert(declitem)
        end
    end
    return result
end


------------------------------------------------------------------------
--  
------------------------------------------------------------------------
function mt:query(...)
    local arg        = select(1, ...)
    local handler    = select(2, ...)
    local handlerobj = select(3, ...)
    if type(arg) == 'string' then
        return querybyname(self, self, arg, handler, handlerobj)
    elseif type(arg) == 'function' then
        return querybyfunc(self, self, arg, handler, handlerobj)
    elseif type(arg) ~= 'table' then
        error('illegal argument for querydecls()', 2)
    end
    local fmap = {
        [decl.FUNC]   = mt.queryfuncs,
        [decl.CLASS]  = mt.queryclasses,
        [decl.METHOD] = mt.querymethods,
        [decl.NS]     = mt.queryns,
    }
    if fmap[arg.tag] then
        return fmap[arg.tag](self, arg, handler, handlerobj)
    end
    
end


------------------------------------------------------------------------
--  
------------------------------------------------------------------------
local function querybytag(set, tag, ...)
    local arg        = select(1, ...)
    local handler    = select(2, ...)
    local handlerobj = select(3, ...)

    if type(arg) == 'string' then
        return querybyname(set, set[tag], arg, handler, handlerobj)
    elseif type(arg) == 'function' then
        return querybyfunc(set, set[tag], arg, handler, handlerobj)
    elseif type(arg) ~= 'table' then
        error(string.format('illegal argument type - "%s" for queryfuncs()', type(arg)), 3)
    end

    if #table.values(arg) == 1 then
        local o = set:new()
        for k,v in ipairs(set[tag]) do
            o:insert(v)
        end
        return o
    end
    return nil
end

function mt:queryfuncs(...)
    return querybytag(self, decl.FUNC, ...)
end

function mt:queryclasses(...)
    return querybytag(self, decl.CLASS, ...)
end

function mt:querymethods(...)
    return querybytag(self, decl.METHOD, ...)
end

function mt:queryns(...)
    return querybytag(self, decl.NS, ...)
end

function mt:queryop(...)
    return querybytag(self, decl.OP, ...)
end

function mt:queryvar(...)
    return querybytag(self, decl.VAR, ...)
end

local function newcell(self, declitem)
    assert(not self.ids[declitem.id], 'newcell() found a existed cell')
    local declids = declitem.ids
    local cellids = self.ids
    local cell = codecell.new{decl=declitem, gen=self.gen, ids=cellids}
    --give gen a chance to customize cell 
    if cell.gen and cell.gen.newcell then
        cell = cell.gen.newcell(cell)
    end
    cellids[cell.id] = cell

   --add OperatorFunction to related cells
    if cell.tag == decl.OPFUNC then
        for i,v in ipairs(cell.args) do
            local info = declitem:typeinfo(v.type)
            local typedecl = info.decl
            if typedecl and typedecl:isclass() then
                local typecell = self.ids[typedecl.id]
                if not typecell then
                    --add codecell of typedecl to codeset's idsmap
                    typecell = self:newcell(typedecl)
                end
                typecell:addsub(cell.tag, cell)
            end
        end
    end
    return cell
end

local function genlevel(cell)
    assert(cell.baselist, 'call genlevel() for no Class or Struct cell')
    if cell.level then return cell.level end
    if #cell.baselist==0 then
        cell.level=0
        return 0
    end
    local level = 0
    for i,v in ipairs(cell.baselist) do
        level = level + genlevel(cell.ids[v.type])
    end
    cell.level = level + 1
    return cell.level
end

local function sortclasses(set)
    for i,v in ipairs(set) do
        genlevel(v)
    end
    table.sort(set, function (a,b) return a.level < b.level end)
end
------------------------------------------------------------------------
--  traverse(hdlmap) for codeset.
--  hdlmap can be:
--  {
--      Tag = TagHandler
--  }
--  which TagHandler's prototype is TagHandler(codeset, codecell),
--  or
--  hdlmap can just be a TagHandler
------------------------------------------------------------------------
local function traverse(self, hdlmap)
    local istab = type(hdlmap) == 'table'
    local classes = {}
    for i,v in ipairs(self) do
        if v:isclass() then
            table.insert(classes, v)
        elseif not v:ismember() then
            local handler = istab and hdlmap[v.tag] or hdlmap
            if handler then
                handler(self, v)
            end
        end
    end
    sortclasses(classes)
    for i,v in ipairs(classes) do
        local handler = istab and hdlmap[v.tag] or hdlmap
        if handler then
            handler(self, v)
        end
    end
end

------------------------------------------------------------------------
--  new() for codeset
------------------------------------------------------------------------
local function codesetctor(self, o)
    o            = o or {}
    o.genname    = self.genname
    o.gen        = self.gen
    o.newcell    = self.newcell
    o.traverse   = self.traverse
    o.new        = self.new
    o.ids        = self.ids
    o            = new(o)
    return o
end

function mt:newcodeset(genname, adddpends)
    adddpends = adddpends == nil and true or adddpends
    genname = genname or 'luabind' -- luabind is default target
    assert(type(genname) == 'string', 'argument "genname" must be a string')

    local gen = require('lgx.gens.'..genname)
    assert(gen, string.format('no generator implemented for "%s"', genname))
    local codeset = new{
        genname    = genname,
        gen        = gen,
        newcell    = newcell,
        new        = codesetctor,
        traverse   = traverse,
    }
    for k,v in pairs(self.ids) do
        if not codeset.ids[k] then
            codeset:newcell(v)
        end
    end
    for i,v in ipairs(self) do
        if adddpends then
            codeset:insertx(codeset.ids[v.id])
        else
            codeset:insert(codeset.ids[v.id])
        end
    end
    return codeset
end

